/* jstile.sprite.js
 * 
 * The MIT License
 * 
 * Copyright (c) 2009 Christiaan Baartse <christiaan@baartse.nl>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
(function(){
	var window = this;
	if(!window.jstile) window.jstile = {};
	var jstile = window.jstile;
	
	jstile.HORIZONTAL = 1;
	jstile.VERTICAL = 2;

	/**
	 * Object holding all image objects using the src as key
	 * @var Object
	 */
	var images = {};

	/**
	 * Image Class
	 * @param String	src			Image url or false to create a invisible sprite
	 * @param Int		width		Width of the image (not the whole image just what's shown)
	 * @param Int		height		Height of the image (not the whole image just what's shown)
	 * @param Int		direction	Direction of the animation (which automatically means that the multi image is picked using the other axis)
	 * @param Int		frames		Amount of frames starting with 0 like the frame parameter
	 * @param Int		speed		Milliseconds between frames (0 = dont animate) (default = 0)
	 */
	jstile.Image = function(src, width, height, direction, frames, speed)
	{
		this.src = src;
		this.width = width;
		this.height = height;
		this.direction = (direction !== undefined) ? direction : jstile.HORIZONTAL;
		this.frames = (frames !== undefined) ? frames : 0;
		this.speed = (speed !== undefined) ? speed : 0;

		if(src) {
			if(typeof(images[this.src]) == 'undefined') {
				var node = new Image();
				node.failed = false;
				node.loaded = -1;
				node.onload = function(){
					this.loaded = 1;
				};
				node.onerror = function(){
					this.loaded = 0;
					throw this.src+' could not be found';
				};
				node.src = this.src;
				images[this.src] = node;
			}
			
			this.node = images[this.src];
		}
		else {
			this.node = false;
		}
	};
	
	/**
	 * Sprite Class
	 * @param jstile.Image|Bool	image			jstile.Image object or false for creating a layer
	 * @param Int				left			x coordinate on layer
	 * @param Int				top				y coordinate on layer
	 * @param Int				zindex			Z-index
	 * @param Int				frame			The current frame
	 * @param Function			onDrawCallback	Callback that gets executed right before the sprite gets drawn
	 * @param Bool				once			Just run the animation once
	 * @param Function			onDone			Callback that is executed when the animation resets/finishes
	 */
	jstile.Sprite = function(image, left, top, zindex, frame, onDrawCallback, once, onDone)
	{
		this.id = ++jstile.Sprite.lastid;
		this.left = left;
		this.top = top;
		this.originalZindex = zindex;
		this.frame = (frame !== undefined) ? frame : 0;
		this.once = !!once;
		this.onDone = onDone;
		this.onDrawCallback = onDrawCallback;

		this.active = true;
		this.layer = false;
		this.parent = false;
		this.children = {};
		this.currentframe = 0;
		this.updateZindex(); // sets this.zindex
		this.updatePosition(); // sets this.position
		this.setImage(image);
	};

	jstile.Sprite.lastid = 0;
	jstile.Sprite.recyclebin = [];
	
	jstile.Sprite.factory = function(image, left, top, zindex, frame, onDrawCallback, once, onDone)
	{
		if(jstile.Sprite.recyclebin.length) {
			var sprite = jstile.Sprite.recyclebin.shift();
			jstile.Sprite.call(sprite, image, left, top, zindex, frame, onDrawCallback, once, onDone);
			return sprite;
		}
		else {
			return new this(image, left, top, zindex, frame, onDrawCallback, once, onDone);
		}
	};

	/**
	 * Set a new image
	 * @param jstile.Image|bool		image
	 * @return jstile.Sprite
	 */
	jstile.Sprite.prototype.setImage = function(image)
	{
		this.image = image;
		this.currentframe = 0;
		this.updateAnimRate();
		this.updateOffset(); // sets this.offset
		
		return this;
	};
	
	/**
	 * Get the offsets in the source image for the current frames
	 * @return Object Object containing left and top properties
	 */
	jstile.Sprite.prototype.updateOffset = function()
	{
		var xframe, yframe;
		switch (this.image.direction)
		{
			case jstile.VERTICAL:
				xframe = this.frame;
				yframe = this.currentframe;
			break;
			case jstile.HORIZONTAL:
			default:
				xframe = this.currentframe;
				yframe = this.frame;
			break;
		}

		this.offset = {
			left : xframe * this.image.width,
			top : yframe * this.image.height
		};
		
		return this;
	};

	/**
	 * Get the offsets in the current image for the current frames
	 * @return Object Object containing left and top properties
	 */
	jstile.Sprite.prototype.updatePosition = function()
	{
		var position = {};
		
		if(this.parent instanceof jstile.Sprite)
		{
			position = {
				left : this.parent.position.left + this.left,
				top : this.parent.position.top + this.top
			};
		}
		else
		{
			position = {
				left : this.left,
				top : this.top
			};
		}
		
		this.position = position;
		
		for (var id in this.children) {
			this.children[id].updatePosition();
		}
		
		return this;
	};

	/**
	 * Gets the Zindex relative from its parent
	 * @return Int
	 */
	jstile.Sprite.prototype.updateZindex = function()
	{
		if(this.parent instanceof jstile.Sprite)
		{
			this.zindex = this.parent.zindex + this.originalZindex;
		}
		else
		{
			this.zindex = this.originalZindex;
		}
		
		for (var id in this.children) {
			this.children[id].updatePosition();
		}
		
		return this;
	};
	
	/**
	 * Calculates the animation rate using the layers FPS
	 * @return this
	 */
	jstile.Sprite.prototype.updateAnimRate = function()
	{
		if(this.image.frames && this.layer) {
			// We know the FPS so we can normalize the ms to our FPS
			this.animRate = parseInt(this.image.speed / this.layer.framerateMs);
			this.idleCount = 0;
		}
		else {
			this.animRate = false;
			this.idleCount = false;
		}
		
		return this;
	};

	/**
	 * Set the sprite on a different image
	 * @param frame
	 */
	jstile.Sprite.prototype.setFrame = function(frame)
	{
		this.frame = frame;
		this.updateOffset();
	};

	/**
	 * Draw the sprite to the layer
	 * @link https://developer.mozilla.org/en/Canvas_tutorial/Using_images#Slicing
	 * @param jstile.Layer layer
	 * @return Bool Returns if the image did get drawn
	 */
	jstile.Sprite.prototype.draw = function(layer)
	{
		var image = this.image;
		
		if(image && this.active && image.node.complete && image.node.loaded) {
			var width = image.width,
			height = image.height,
			sx = this.offset.left,
			sy = this.offset.top,
			dx = this.position.left,
			dy = this.position.top;
			
			// Do we try to draw at a position outside of the layer
			if(dx < 0)
			{
				if(dx + width < 0) return false;
				var px = (0 - dx); // Amount we're overdue
				width -= px;// Cut it off the width
				sx += px;
				dx = 0;
			}
			// If the image get drawn completely outside skip it
			else if(dx > layer.width)
			{
				return false;
			}
			else if(dx + width > layer.width)
			{
				width = (layer.width - dx); // Width is the remaining width
			}
			
			if(dy < 0)
			{
				if(dy + height < 0) return false;
				var px = (0 - dy); // Amount we're overdue
				height -= px;// Cut it off the height
				sy += px;
				dy = 0;
			}
			else if(dy > layer.height)
			{
				return false;
			}
			else if(dy + height > layer.height)
			{
				height = (layer.height - dy);// Height is the remaining width
			}
			
			if(!width || !height) {
				return false;
			}
			
			layer.context.drawImage(image.node, sx, sy, width, height, dx, dy, width, height);
			
			if(image.frames)
			{
				if(this.idleCount == this.animRate) {
					this.idleCount = 0;
					// Advance the animation
					if(++this.currentframe > this.image.frames)
					{
						this.currentframe = 0;
						if(this.onDone) {
							this.onDone();
						}
						
						if(this.once) {
							this.remove();
						}
					}
					this.updateOffset();
				}
				else if(this.idleCount !== false) {
					++this.idleCount;
				}
			}
			
			return true;
		}
		return false;
	};

	/**
	 * Set or unset the layer
	 * @param Layer 	layer
	 * @return jstile.Sprite
	 */
	jstile.Sprite.prototype.setLayer = function(layer)
	{
		this.layer = layer;
		
		this.updateAnimRate();
		
		for ( var id in this.children) {
			this.children[id].setLayer(layer);
		}

		return this;
	};
	
	/**
	 * Set or unset the parent of the sprite
	 * @param parent
	 * @return jstile.Sprite
	 */
	jstile.Sprite.prototype.setParent = function(parent)
	{
		if(parent !== false)
		{
			if(this.parent !== false) {
				throw 'Sprite already has a parent remove it first';
			}
			this.parent = parent;
			this.updateZindex();
			this.updatePosition();
		}
		else
		{
			this.parent = false;
			this.updateZindex();
			this.updatePosition();
		}
		return this;
	};
	
	/**
	 * Add a child
	 * @param jstile.Sprite	child
	 * @param Bool			noprepare
	 * @return jstile.Sprite	this
	 */
	jstile.Sprite.prototype.addChild = function(child, noprepare)
	{
		if(!(child instanceof Array)) {
			child = [child];
		}
		
		for(var i = 0, len = child.length; i < len; i++)
		{
			child[i].setParent(this);
			this.children[child[i].id] = child[i];
			if(this.layer) {
				this.layer.addObject(child[i], noprepare);
			}
		}
		return this;
	};
	
	/**
	 * Remove a child
	 * @param jstile.Sprite	child
	 * @param Bool			noprepare
	 * @return jstile.Sprite	this
	 */
	jstile.Sprite.prototype.removeChild = function(child, noprepare)
	{
		if(!(child instanceof Array)) {
			child = [child];
		}
		
		for(var i = 0, len = child.length; i < len; i++)
		{
			if(this.layer) {
				this.layer.removeObject(child[i], noprepare);
			}
			delete this.children[child[i].id];
			child[i].setParent(false);
		}
		return this;
	};	
	
	/**
	 * Does the Sprite contain the given child
	 * @param jstile.Sprite	child
	 * @return Bool
	 */
	jstile.Sprite.prototype.hasChild = function(child)
	{
		return typeof(this.children[child.id]) !== 'undefined';
	};
	
	/**
	 * Unregister the sprite from its parent and layer
	 * @param	Bool	nodeep
	 * @return	jstile.Sprite
	 */
	jstile.Sprite.prototype.remove = function(nodeep)
	{
		if(!nodeep) {
			for ( var id in this.children) {
				this.children[id].remove(nodeep);
			}
		}
		
		if(this.parent) {
			this.parent.removeChild(this);
		}
		else if(this.layer) {
			this.layer.removeObject(this);
		}
		
		this.id = false;
		this.active = false;
		jstile.Sprite.recyclebin.push(this);
	};
	
	/**
	 * Is this sprite somewhere shown on the layer
	 * or is it completely outside of the layer
	 * @return Bool
	 */
	jstile.Sprite.prototype.inViewport = function()
	{
		var offset = this.position;
		return (
			offset.top <= this.layer.height
			&& offset.left <= this.layer.width 
			&& offset.top + this.image.height >= 0
			&& offset.left + this.image.width >= 0
		);
	};
	
	/**
	 * Test for collisions with other sprites
	 * @param	Array	Array with sprites to test for
	 * @return	Array	Array containing colliding sprites
	 */
	jstile.Sprite.prototype.collides = function(sprites)
	{
		var result = [], i, sprite, thisOffset = this.position,
		bottom = thisOffset.top + this.image.height,
		right = thisOffset.left + this.image.width;
		
		if(this.inViewport()) {
			for (i = sprites.length-1; i >= 0; --i) {
				sprite = sprites[i];
				
				if(sprite instanceof Array) {
					result = result.concat(this.collision(sprite));
				}
				else {
					var thatOffset = sprite.position
					// If we not not touch ;)
					if(!(
						   thatOffset.top+sprite.image.height < thisOffset.top		// we're to far to the top to touch
						|| thatOffset.left+sprite.image.width < thisOffset.left	// we're to far to the right to touch
						|| thatOffset.top > bottom	// we're to far to the bottom to touch
						|| thatOffset.left > right	// we're to far to the left to touch
					)){
						result.push(sprite);
					}
				}
			}
		}
		
		return result;
	};
})();